#include "Volume.h"
#include <HybridRenderer\Scene\Camera.h>

#include <HybridRenderer\Render\ATTRIB.h>
#include <HybridRenderer\Render\ShaderConstants.h>

#include <PolyVoxCore/GradientEstimators.h>
#include <PolyVoxCore/MaterialDensityPair.h>
#include <PolyVoxCore/MeshDecimator.h>
#include <PolyVoxCore/CubicSurfaceExtractorWithNormals.h>
#include <PolyVoxCore/CubicSurfaceExtractor.h>
#include <PolyVoxCore/SurfaceExtractor.h>

#include <vector>

using namespace Scene;
using namespace Eigen;
using namespace PolyVox;

Volume::Volume()
	: max_height(95), heightmap(512,512), volData(SimpleVolume<MaterialDensityPair44>(PolyVox::Region(Vector3DInt32(0,0,0), Vector3DInt32(511,511,95))))
{
	//createSphereInVolume(30.0f, 5);
	createTerrainInVolume(4);

	mesh = std::unique_ptr<OpenGLSurfaceMesh>(new OpenGLSurfaceMesh());
	//CubicSurfaceExtractorWithNormals<SimpleVolume, MaterialDensityPair44> surfaceExtractor(&volData, volData.getEnclosingRegion(), &mesh->sourceMesh);
	SurfaceExtractor<SimpleVolume, MaterialDensityPair44> surfaceExtractor(&volData, volData.getEnclosingRegion(), &mesh->sourceMesh);
	//CubicSurfaceExtractor<SimpleVolume, MaterialDensityPair44> surfaceExtractor(&volData, volData.getEnclosingRegion(), &mesh->sourceMesh);
	surfaceExtractor.execute();

	BuildOpenGLSurfaceMesh();
}

void Volume::render(const Render::ShaderPtr &active_program)
{
	unsigned int uLodLevel = 0;												
	renderVertexBufferObject(active_program, uLodLevel);
}

void Volume::createSphereInVolume(float fRadius, uint8_t uValue)
{
	//This vector hold the position of the center of the volume
	Vector3DInt32 v3dVolCenter = (volData.getEnclosingRegion().getUpperCorner() - volData.getEnclosingRegion().getLowerCorner()) / 2 + Vector3DInt32(1,1,1);

	//This three-level for loop iterates over every voxel in the volume
	for (int z = 0; z < volData.getWidth(); z++)
	{
		for (int y = 0; y < volData.getHeight(); y++)
		{
			for (int x = 0; x < volData.getDepth(); x++)
			{
				//Store our current position as a vector...
				Vector3DInt32 v3dCurrentPos(x,y,z);	
				//And compute how far the current position is from the center of the volume
				double fDistToCenter = (v3dCurrentPos - v3dVolCenter).length();

				//If the current voxel is less than 'radius' units from the center
				//then we make it solid, otherwise we make it empty space.
				if(fDistToCenter <= fRadius)
				{
					volData.setVoxelAt(x,y,z, MaterialDensityPair44(uValue, uValue > 0 ? MaterialDensityPair44::getMaxDensity() : MaterialDensityPair44::getMinDensity()));
				}
			}
		}
	}
}

void Volume::createTerrainInVolume(uint8_t uValue)
{
	TCODRandom ran(100);
	TCODNoise noise(2, &ran);
	//heightmap.addHill(256,256, 90,120);
	heightmap.addFbm(&noise, 0.8f, 0.8f, 0.0f, 0.0f, 12.0f, 0.0f, 200.0f);
	heightmap.normalize(0, max_height-1);
	//This three-level for loop iterates over every voxel in the volume
	for (int x = 0; x < volData.getWidth(); x++)
	{
		for (int y = 0; y < volData.getHeight(); y++)
		{
			int index = heightmap.getValue(x,y);
			for(int z = 0; z < index; z++)
				volData.setVoxelAt(x,y,z, MaterialDensityPair44(uValue, uValue > 0 ? MaterialDensityPair44::getMaxDensity() : MaterialDensityPair44::getMinDensity()));
		}
	}
}

void Volume::BuildOpenGLSurfaceMesh()
{
	//Convienient access to the vertices and indices
	auto &vecVertices = mesh->sourceMesh.getVertices();
	auto &vecIndices = mesh->sourceMesh.getIndices();

	std::vector<float> vertices;
	std::vector<float> normals;
	std::vector<float> colors;
	std::vector<float> texcoords;
	for(auto iterVertex = vecVertices.begin(); iterVertex != vecVertices.end(); ++iterVertex)
	{
		const PositionMaterialNormal& vertex = *iterVertex;
		const Vector3DFloat& v3dVertexPos = vertex.getPosition();
		const Vector3DFloat& v3dVertexNormal = vertex.getNormal();

		float z = v3dVertexPos.getZ();
		
		vertices.push_back(v3dVertexPos.getX()*1.0f);
		vertices.push_back(v3dVertexPos.getY()*1.0f);
		vertices.push_back(z*1.0f);

		float n[3];
		heightmap.getNormal((int)v3dVertexPos.getX(), (int)v3dVertexPos.getY(), n);
		normals.push_back(n[0]);
		normals.push_back(n[1]);
		normals.push_back(n[2]);

		Vector3DFloat color;
		if(z < (10.0f/max_height)*100.0f)
			color = Vector3DFloat(71.0/255.0,173.0/255.0,244.0/255.0);
		else if(z < (25.0f/max_height)*100.0f)
			color = Vector3DFloat(186.0/255.0,165.0/255.0,93.0/255.0,1.0);
		else if(z < (60.0f/max_height)*100.0f)
			color = Vector3DFloat(107.0/255.0,142.0/255.0,95.0/255.0,1.0);
		else if(z < (90.0f/max_height)*100.0f)
			color = Vector3DFloat(109.0/255.0,90.0/255.0,68.0/255.0,1.0);
		else
			color = Vector3DFloat(218.0/255.0,224.0/255.0,241.0/255.0,1.0);

		/*uint8_t material = static_cast<uint8_t>(vertex.getMaterial() + 0.5);
		OpenGLColour colour = convertMaterialIDToColour(material);*/

		colors.push_back(color.getX());
		colors.push_back(color.getY());
		colors.push_back(color.getZ());
		/*colors.push_back(1.0f);
		colors.push_back(0.0f);
		colors.push_back(0.0f);*/
		colors.push_back(1.0f);

		texcoords.push_back(CalcBaseFloat(1.00001f, v3dVertexPos.getX()));
		texcoords.push_back(CalcBaseFloat(1.00001f, v3dVertexPos.getY()));

		/*if((int)v3dVertexPos.getX()%2 == 0)
			texcoords.push_back(0.0f);
		else
			texcoords.push_back(1.0f);

		if((int)v3dVertexPos.getY()%2 == 0)
			texcoords.push_back(0.0f);
		else
			texcoords.push_back(1.0f);*/
	}

	mesh->vao = std::make_shared<Render::VAO>();
	mesh->vbo = std::make_shared<Render::VBO>((vertices.size()+normals.size()+colors.size()+texcoords.size()) * sizeof(GLfloat), GL_STATIC_DRAW);
	mesh->ibo = std::make_shared<Render::IBO>(vecIndices, GL_STATIC_DRAW);

	auto v_offset = mesh->vbo->buffer<float>(vertices);
	auto n_offset = mesh->vbo->buffer<float>(normals);
	auto c_offset = mesh->vbo->buffer<float>(colors);
	auto t_offset = mesh->vbo->buffer<float>(texcoords);

	Render::ATTRIB::bind(Render::ShaderConstants::Position(), 3, GL_FLOAT, false, 0, v_offset);
	Render::ATTRIB::bind(Render::ShaderConstants::Normal(),   3, GL_FLOAT, false, 0, n_offset);
	Render::ATTRIB::bind(Render::ShaderConstants::Diffuse(),  4, GL_FLOAT, false, 0, c_offset);
	Render::ATTRIB::bind(Render::ShaderConstants::TexCoord(),  2, GL_FLOAT, false, 0, t_offset);

	mesh->vao->unbind();
	mesh->vbo->unbind();
	mesh->ibo->unbind();
}

void Volume::renderVertexBufferObject(const Render::ShaderPtr &active_program, unsigned int uLodLevel)
{
	Affine3f model = Affine3f::Identity();
	model.matrix()(12) = position.x();
	model.matrix()(13) = position.y();
	model.matrix()(14) = position.z();

	//static float var = 0.f;
	//var += 1e-2;
	model.rotate( AngleAxisf(M_PI,  Vector3f::UnitY()) );

	auto &proj = Camera::getSingleton()->getProjection();
	auto &view = Camera::getSingleton()->getView();

	auto modelView = view * model;
	auto modelViewProj = proj * view * model;
	auto mv_inv4x4 = modelView.inverse();
  
	Matrix3f mv_inv3x3;
	Matrix3f model3x3;

	for(int i=0; i<3; i++){
	for(int j=0; j<3; j++){
		mv_inv3x3(i,j) = mv_inv4x4(i,j);
		model3x3(i,j) = model(i,j);
	}
	}

	auto normal = mv_inv3x3.transpose();
	auto worldRotationInverse = model3x3.transpose();
	Matrix3f normalWorldRotationInverse = normal;

	mvp->bind(modelViewProj.matrix());
	mv->bind(modelView.matrix());
	n_wri->bind(normalWorldRotationInverse);

	int beginIndex = mesh->sourceMesh.m_vecLodRecords[uLodLevel].beginIndex;
	int endIndex = mesh->sourceMesh.m_vecLodRecords[uLodLevel].endIndex;

	if(tex_array)
	{
		glActiveTexture(GL_TEXTURE0);
		tex_array->bind();
		tex_sampler->bind(0);
		sampler_uni->bind(0);
	}
	else if(tex)
	{
		glActiveTexture(GL_TEXTURE0);
		tex->bind();
		tex_sampler->bind(0);
		sampler_uni->bind(0);
	}
	if(material)
		material->bind_id(active_program->getFS());

	mesh->vao->bind();

	glDrawRangeElements(GL_TRIANGLES, beginIndex, endIndex-1, endIndex - beginIndex, GL_UNSIGNED_INT, 0);

	if(tex_array)
		tex_array->unbind();
	else if(tex)
		tex->unbind();
}

Volume::OpenGLColour Volume::convertMaterialIDToColour(uint8_t materialID)
{
	OpenGLColour colour;

	switch(materialID)
	{
	case 1:
		colour.red = 1.0f;
		colour.green = 0.0f;
		colour.blue = 0.0f;
		break;
	case 2:
		colour.red = 0.0f;
		colour.green = 1.0f;
		colour.blue = 0.0f;
		break;
	case 3:
		colour.red = 0.0f;
		colour.green = 0.0f;
		colour.blue = 1.0f;
		break;
	case 4:
		colour.red = 1.0f;
		colour.green = 1.0f;
		colour.blue = 0.0f;
		break;
	case 5:
		colour.red = 1.0f;
		colour.green = 0.0f;
		colour.blue = 1.0f;
		break;
	default:
		colour.red = 1.0f;
		colour.green = 1.0f;
		colour.blue = 1.0f;
	}

	return colour;
}